package org.softmed.rest.server.xstream;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.softmed.persistence.PersistenceManager;
import org.softmed.reflection.utils.FieldInfo;
import org.softmed.reflection.utils.ReflectionUtil;

import org.softmed.rest.config.Application;
import org.softmed.rest.generation.ScafoldFacade;
import org.softmed.rest.generation.fields.GenerationConfig;
import org.softmed.rest.generation.scafold.AppConfig;
import org.softmed.rest.generation.xml.XMLException;
import org.softmed.rest.generation.xml.XMLObjectEdit;
import org.softmed.rest.generation.xml.XMLUnmarshaller;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import com.google.inject.Inject;
import com.google.inject.name.Named;
import com.sun.org.apache.xerces.internal.dom.DeferredTextImpl;

public class ProcessObjectEdit implements XMLObjectEdit {

	@Inject
	@Named("URIFIeldName")
	static String uriFieldName;

	ReflectionUtil rutil = new ReflectionUtil();

	public Object fromXML(String xml, String id, Class type,
			PersistenceManager util, Application app) throws Throwable {

		AppConfig genApp = ScafoldFacade.getApp(app);
		GenerationConfig genCfg = genApp.getGenerationConfig();

		// remove non native fields
		Map<FieldInfo, String> nonNativeFields = new HashMap<FieldInfo, String>();

		Document doc = getDocument(xml);

		List<FieldInfo> infos = rutil.getNonNativeFieldInfos(type);
		for (FieldInfo fieldInfo : infos) {

			String alias = genCfg.getFieldAlias(fieldInfo.getDeclaringClass(),
					fieldInfo.getName());
			if (alias == null)
				alias = rutil.getHifenizedName(fieldInfo.getName());

			Element node = getFieldReferenceInXML(doc, alias);

			if (node == null)
				continue;

			if (List.class.isAssignableFrom(fieldInfo.getType())) {
				if (rutil.isSpecificList(rutil.getField(type, fieldInfo
						.getName())) == null)
					continue;

				// Element child = (Element) nodes.item(0);
				Element child = node;
				String completeListXML = toString(child);

				child.getParentNode().removeChild(child);

				nonNativeFields.put(fieldInfo, completeListXML);
			} else {

				// Element child = (Element) nodes.item(0);
				Element child = node;
				String uri = child.getAttribute(uriFieldName);
				if (uri == null)
					continue;

				child.getParentNode().removeChild(child);

				nonNativeFields.put(fieldInfo, toString(child));

			}
		}

		String shortXML = toString(doc);

		Object object = getObject(type, id, shortXML, util);

		// CustomDefaultXMLUnmarshaller converter = new
		// CustomDefaultXMLUnmarshaller(
		// util, type, app);

		CustomDefaultXMLUnmarshaller converter = XMLCache.getUnmarshaller(type,
				util, app);

		if (object != null) {
			// apply native values
			Object changed = converter.fromXML(util,shortXML);

			// if the unmarshalled object is of another class
			// than the target object, return the unmarshalled object
			// test fro exception
			if (changed.getClass() != object.getClass())
				throw new XMLException(
						"Unmarshalled object is of a different class than the target class!");

			applyChangedNativeFieldValues(type, util, shortXML, object,
					changed, genCfg);

		} else {
			// unmarshal from xml
			object = converter.fromXML(util,shortXML);
		}

		// go through list of removed xml fields
		// recursive call set by reflection
		Set<Entry<FieldInfo, String>> entries = nonNativeFields.entrySet();
		for (Entry<FieldInfo, String> entry : entries) {
			FieldInfo field = entry.getKey();
			String sxml = entry.getValue();
			if (List.class.isAssignableFrom(field.getType())) {
				Class listType = rutil.isSpecificList(rutil.getField(type,
						field.getName()));

				if (listType == null)
					continue;

				Document sdoc = getDocument(sxml);
				if (sdoc.getDocumentElement().getChildNodes().getLength() == 0)
					continue;

				List list = (List) field.getGetter().invoke(object, null);
				// clear list is specifically indicated in the xml
				list.clear();

				addItemsToList(sdoc, list, listType, converter, util, app);

			} else {

				// App genApp = ScafoldFacade.getApp(app);
				// GenerationConfig genCfg = genApp.getGenerationConfig();

				String alias = genCfg.getAlias(field.getType());
				if (alias == null)
					alias = rutil.getHifenizedName(field.getType()
							.getSimpleName());

				String fieldAlias = genCfg.getFieldAlias(field
						.getDeclaringClass(), field.getName());
				if (fieldAlias == null)
					fieldAlias = rutil.getHifenizedName(field.getName());

				if (!alias.equals(fieldAlias)) {
					Document tdoc = getDocument(sxml);
					Element childE = tdoc.getDocumentElement();
					Node item = childE.getFirstChild().getNextSibling();
					sxml = toString(item);
					// doc = getDocument(sxml);
				}

				Object child = fromXML(sxml, null, field.getType(), util, app);
				field.getSetter().invoke(object, new Object[] { child });
			}
		}

		return object;

	}

	protected Element getFieldReferenceInXML(Document doc, String alias) {
		Element node = null;
		Element temp = null;
		Node tnode = null;
		Element rootElement = doc.getDocumentElement();
		// rootElement.getChildNodes();
		NodeList childNodes = rootElement.getChildNodes();

		for (int i = 0; i < childNodes.getLength(); i++) {
			tnode = childNodes.item(i);

			if (!(tnode instanceof Element))
				continue;

			if (tnode.getNextSibling() == null)
				continue;

			temp = (Element) tnode;
			String nodeName = temp.getNodeName();
			if (nodeName.equals(alias)) {
				node = temp;
				break;
			}
		}
		return node;
	}

	private void addItemsToList(Document doc, List list, Class listType,
			CustomDefaultXMLUnmarshaller converter, PersistenceManager util,
			Application app) throws Throwable {
		// Document doc = getDocument(xml);
		Element child = doc.getDocumentElement();
		Node listItem = child.getFirstChild().getNextSibling();

		Element childItem = null;
		do {
			if (listItem instanceof DeferredTextImpl)
				listItem = listItem.getNextSibling();

			if (listItem == null)
				break;

			childItem = (Element) listItem;

			String uri = childItem.getAttribute(uriFieldName);
			Object childObject = null;
			String childXML = toString(childItem);
			childObject = fromXML(childXML, null, listType, util, app);

			list.add(childObject);
		} while ((listItem = listItem.getNextSibling()) != null);

	}

	protected void applyChangedNativeFieldValues(Class type,
			PersistenceManager util, String shortXML, Object object,
			Object changed, GenerationConfig genCfg) throws SAXException,
			IOException, ParserConfigurationException, Throwable,
			IllegalAccessException, InvocationTargetException {
		Document doc;
		// auto generated fields are not used....
		Collection<FieldInfo> generatedFields = util.getGeneratedFields(type);
		doc = getDocument(shortXML);

		// get list of specified attributes and fields
		List<FieldInfo> changedFields = new ArrayList<FieldInfo>();
		List<FieldInfo> nativeFieldInfos = rutil.getNativeFieldInfos(type);
		Element root = doc.getDocumentElement();
		NamedNodeMap atts = root.getAttributes();

		for (int i = 0; i < atts.getLength(); i++) {
			Node att = atts.item(i);

			// the attribute name must equal a field from the class to be
			// applied
			FieldInfo info = getFieldInfo(att.getNodeName(), nativeFieldInfos,
					type, genCfg);
			if (info == null)
				continue;

			// auto generated fields are not applied
			if (isFieldGenerated(info.getName(), generatedFields, type, genCfg) != null)
				continue;

			changedFields.add(info);
		}
		// missing child nodes
		NodeList childs = root.getChildNodes();
		for (int i = 0; i < childs.getLength(); i++) {
			Node child = childs.item(i);

			// the attribute name must equal a field from the class to be
			// applied
			FieldInfo info = getFieldInfo(child.getNodeName(),
					nativeFieldInfos, type, genCfg);
			if (info == null)
				continue;

			// auto generated fields are not applied
			if (getFieldInfoNormalName(info.getName(), generatedFields, type,
					genCfg) != null)
				continue;

			changedFields.add(info);

		}

		Collection<FieldInfo> genFields = util.getGeneratedFields(type);
		// apply native values through reflection - the ones indicated in
		// the
		// remaining xml
		for (FieldInfo changedField : changedFields) {
			// if(changedField.getName())
			Object value = changedField.getGetter().invoke(changed, null);
			changedField.getSetter().invoke(object, new Object[] { value });
		}
	}

	protected FieldInfo getFieldInfo(String hifenizedName,
			Collection<FieldInfo> infos, Class type, GenerationConfig genCfg) {
		for (FieldInfo fieldInfo : infos) {
			String alias = genCfg.getFieldAlias(fieldInfo.getDeclaringClass(),
					fieldInfo.getName());
			if (alias == null)
				alias = rutil.getHifenizedName(fieldInfo.getName());

			if (alias.equals(hifenizedName))
				return fieldInfo;
		}

		return null;
	}

	// TODO this is for generated fields, applied same fix as in the
	// previous method, be sure this works for Primary Key aliasing
	protected FieldInfo getFieldInfoNormalName(String fieldName,
			Collection<FieldInfo> infos, Class type, GenerationConfig genCfg) {
		if (infos != null)
			for (FieldInfo fieldInfo : infos) {

				String alias = genCfg.getFieldAlias(fieldInfo
						.getDeclaringClass(), fieldInfo.getName());
				if (alias == null)
					alias = rutil.getHifenizedName(fieldInfo.getName());

				if (alias.equals(fieldName))
					return fieldInfo;
			}

		return null;
	}

	protected FieldInfo isFieldGenerated(String fieldName,
			Collection<FieldInfo> infos, Class type, GenerationConfig genCfg) {
		if (infos != null)
			for (FieldInfo fieldInfo : infos) {

				if (fieldInfo.getName().equals(fieldName))
					return fieldInfo;
			}

		return null;
	}

	protected Object getObject(Class type, String id, String shortXML,
			PersistenceManager util) throws Throwable {

		if (id != null)
			return util.getById(type, id);

		Document doc = getDocument(shortXML);
		Element root = doc.getDocumentElement();

		// load from odb and return if xml didn't contain any related info
		String uri = root.getAttribute(uriFieldName);
		if (uri != null && uri.length() > 0) {
			String objectId = uri.substring(uri.lastIndexOf('/') + 1);
			return util.getById(type, objectId);
		}

		return null;

	}

	public String toString(Node node) throws Throwable {
		StringWriter sw = new StringWriter();
		Transformer serializer = TransformerFactory.newInstance()
				.newTransformer();
		serializer.transform(new DOMSource(node), new StreamResult(sw));
		String string = sw.toString();
		if (string.startsWith("<?xml"))
			string = string.substring(string.indexOf('>') + 1);
		return string;
	}

	private Document getDocument(String xml) throws SAXException, IOException,
			ParserConfigurationException {
		DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory
				.newInstance();
		Document doc = documentBuilderFactory.newDocumentBuilder().parse(
				new ByteArrayInputStream((xml)
						.getBytes(XStreamManager.CHAR_SET)));
		return doc;
	}

	public ProcessObjectEdit clone() {
		return new ProcessObjectEdit();
	}

}
